package com.chaos.gateway.config;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

import com.chaos.framework.model.constant.Constants;
import com.chaos.framework.model.constant.SecurityConstants;
import com.chaos.gateway.config.properties.ResourceserverProperties;
import com.chaos.gateway.entity.*;
import com.chaos.gateway.helper.JwtTokenHelper;
import com.chaos.gateway.service.auth.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author Jacky
 */
@Component
@Slf4j
public class ResourceServerManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    private final PathMatcher pathMatcher = new AntPathMatcher();
    @Resource
    private ResourceserverProperties resourceserverProperties;
    @Resource
    private OauthClientDetailsPermissionService oauthClientDetailsPermissionService;
    @Resource
    private OauthClientDetailsService oauthClientDetailsService;
    @Resource
    private RbacPermissionService rbacPermissionService;
    @Resource
    private RbacUserRoleService rbacUserRoleService;
    @Resource
    private RbacRoleMenuBtnService rbacRoleMenuBtnService;
    @Resource
    private RbacPermissionMenuBtnService rbacPermissionMenuBtnService;

    private Mono<AuthorizationDecision> yesAccess() {
        return Mono.just(new AuthorizationDecision(true));
    }

    private Mono<AuthorizationDecision> noAccess() {
        return Mono.just(new AuthorizationDecision(false));
    }

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
        ServerHttpRequest request = authorizationContext.getExchange().getRequest();
        // 预检请求放行
        if (request.getMethod() == HttpMethod.OPTIONS) {
            return yesAccess();
        }

        // 健康检查白名单放行
        if (antPathMatcher.match(SecurityConstants.ACTUATOR_MATCH, request.getURI().getPath())) {
            InetSocketAddress remoteAddress = request.getRemoteAddress();
            if (remoteAddress != null) {
                String ip = remoteAddress.getAddress().toString().replace("/", "");
                if (resourceserverProperties.getActuatorHosts().contains(ip)) {
                    return yesAccess();
                }
            }
        }

        String method = request.getMethodValue();
        String path = request.getURI().getPath();
        String restfulPath = method + ":" + path;

        String token = request.getHeaders().getFirst(SecurityConstants.AUTHORIZATION_KEY);
        if (!StrUtil.isNotBlank(token) || !StrUtil.startWithIgnoreCase(token, SecurityConstants.JWT_PREFIX)) {
            return noAccess();
        }
        JSONObject tokenObject = JwtTokenHelper.getTokenMap(token);

        // openApi
        if (pathMatcher.match(SecurityConstants.OPEN_API_PATTERN, path)) {
            return checkOpenApi(tokenObject, restfulPath);
        }

        // api
        if (pathMatcher.match(SecurityConstants.ADMIN_API_PATTERN, path)) {
            //默认是基于权限的 api 管控
            return checkAdminApi(tokenObject, restfulPath);
        }

        return checkApi(tokenObject);

    }

    private Mono<AuthorizationDecision> checkAdminApi(JSONObject tokenObject, String restfulPath) {
        String adminId = tokenObject.getStr(Constants.ADMIN_ID);

        QueryWrapper<RbacUserRole> userRoleQuery = new QueryWrapper<>();
        userRoleQuery.lambda().eq(RbacUserRole::getUserId, adminId);
        List<RbacUserRole> userRoles = rbacUserRoleService.list(userRoleQuery);
        if (ObjectUtil.isEmpty(userRoles)) {
            return noAccess();
        }
        List<Long> roleIds = userRoles.stream().map(RbacUserRole::getRoleId).collect(Collectors.toList());
        if (roleIds.contains(1L)) {
            //超级管理员
            return yesAccess();
        }
        QueryWrapper<RbacPermission> queryPerm = new QueryWrapper<>();
        queryPerm.lambda().eq(RbacPermission::getUrlPerm, restfulPath);
        List<RbacPermission> perms = rbacPermissionService.list(queryPerm);
        if (ObjectUtil.isEmpty(perms) || perms.size() > 1) {
            return noAccess();
        }
        RbacPermission perm = perms.get(0);
        QueryWrapper<RbacPermissionMenuBtn> permMenuBtnQuery = new QueryWrapper<>();
        permMenuBtnQuery.lambda()
                .eq(RbacPermissionMenuBtn::getPermissionId, perm.getId());

        List<RbacPermissionMenuBtn> menus = rbacPermissionMenuBtnService.list(permMenuBtnQuery);

        Set<Long> menuIds = menus.stream().map(RbacPermissionMenuBtn::getMenuOrBtnId).collect(Collectors.toSet());

        QueryWrapper<RbacRoleMenuBtn> roleMenuBtnQuery = new QueryWrapper<>();
        roleMenuBtnQuery.lambda()
                .in(RbacRoleMenuBtn::getMenuOrBtnId, menuIds);
        List<RbacRoleMenuBtn> roles = rbacRoleMenuBtnService.list(roleMenuBtnQuery);

        Set<Long> needRoleIds = roles.stream().map(RbacRoleMenuBtn::getRoleId).collect(Collectors.toSet());

        roleIds.retainAll(needRoleIds);

        if (!roleIds.isEmpty()) {
            return yesAccess();
        }
        return noAccess();


    }

    private Mono<AuthorizationDecision> checkApi(JSONObject tokenObject) {
        Object userId = tokenObject.get(Constants.USER_ID);
        Object tenantId = tokenObject.get(Constants.TENANT_ID);
        boolean granted = ObjectUtil.isNotEmpty(userId) && ObjectUtil.isNotEmpty(tenantId);
        return Mono.just(new AuthorizationDecision(granted));
    }

    private Mono<AuthorizationDecision> checkOpenApi(JSONObject tokenObject, String restfulPath) {
        String clientId = tokenObject.getStr(Constants.CLIENT_ID);

        if (StringUtils.isBlank(clientId)) {
            return noAccess();
        }

        QueryWrapper<OauthClientDetails> query = new QueryWrapper<>();
        query.lambda().eq(OauthClientDetails::getClientId, clientId);
        List<OauthClientDetails> clients = oauthClientDetailsService.list(query);
        if (ObjectUtil.isEmpty(clients)) {
            return noAccess();
        }

        QueryWrapper<RbacPermission> queryPerm = new QueryWrapper<>();
        queryPerm.lambda().eq(RbacPermission::getUrlPerm, restfulPath).eq(RbacPermission::getType, 2);
        List<RbacPermission> perms = rbacPermissionService.list(queryPerm);
        if (ObjectUtil.isEmpty(perms) || perms.size() > 1) {
            return noAccess();
        }
        RbacPermission perm = perms.get(0);

        QueryWrapper<OauthClientDetailsPermission> queryClientPerm = new QueryWrapper<>();
        queryClientPerm.lambda().eq(OauthClientDetailsPermission::getPermissionId, perm.getId()).eq(OauthClientDetailsPermission::getClientId, clientId);

        List<OauthClientDetailsPermission> clientPerms = oauthClientDetailsPermissionService.list(queryClientPerm);
        return !clientPerms.isEmpty() ? yesAccess() : noAccess();
    }

}
